/****************************************************************************** * Copyright (c) 2006, 2010 VMware Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * and Apache License v2.0 which accompanies this distribution. * The Eclipse Public License is available at * http://www.eclipse.org/legal/epl-v10.html and the Apache License v2.0 * is available at http://www.opensource.org/licenses/apache2.0.php. * You may elect to redistribute this code under either of these licenses. * * Contributors: * VMware Inc. *****************************************************************************/ package org.eclipse.gemini.blueprint.blueprint.config.internal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.eclipse.gemini.blueprint.blueprint.config.internal.support.InstanceEqualityRuntimeBeanReference; import org.eclipse.gemini.blueprint.blueprint.reflect.internal.support.OrderedManagedProperties; import org.springframework.beans.PropertyValue; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.config.RuntimeBeanNameReference; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.config.TypedStringValue; import org.springframework.beans.factory.parsing.BeanEntry; import org.springframework.beans.factory.parsing.ConstructorArgumentEntry; import org.springframework.beans.factory.parsing.ParseState; import org.springframework.beans.factory.parsing.PropertyEntry; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; import org.springframework.beans.factory.support.ManagedArray; import org.springframework.beans.factory.support.ManagedList; import org.springframework.beans.factory.support.ManagedMap; import org.springframework.beans.factory.support.ManagedProperties; import org.springframework.beans.factory.support.ManagedSet; import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * Stateful class that handles the parsing details of a <component> elements. Borrows heavily from * {@link BeanDefinitionParserDelegate}. * * <b>Note</b>: Due to its stateful nature, this class is not thread safe. * * <b>Note</b>: Since the namespace is important when parsing elements and since mixed elements, from both rfc124 and * Spring can coexist in the same file, reusing the {@link BeanDefinitionParserDelegate delegate} isn't entirely * possible since the two state needs to be kept in synch. * * @author Costin Leau */ public class BlueprintParser { /** logger */ private static final Log log = LogFactory.getLog(BlueprintParser.class); public static final String BEAN = "bean"; public static final String COMPONENT_ID_ATTR = "component-id"; public static final String CONSTRUCTOR_ARG = "argument"; private static final String FACTORY_REF_ATTR = "factory-ref"; private static final String LAZY_INIT_ATTR = "activation"; private static final String LAZY_INIT_VALUE = "lazy"; private static final String EAGER_INIT_VALUE = "eager"; public static final String NAMESPACE_URI = "http://www.osgi.org/xmlns/blueprint/v1.0.0"; public static final String DECLARED_SCOPE = "org.eclipse.gemini.blueprint.blueprint.xml.bean.declared.scope"; private final ParseState parseState; private final Collection<String> usedNames; private ParserContext parserContext; private BlueprintDefaultsDefinition defaults; public BlueprintParser() { this(null, null); } /** * Constructs a new <code>ComponentParser</code> instance. Used by certain reusable static methods. * * @param parserContext */ private BlueprintParser(ParserContext parserContext) { this(null, null); this.parserContext = parserContext; } public BlueprintParser(ParseState parseState, Collection<String> usedNames) { this.parseState = (parseState != null ? parseState : new ParseState()); this.usedNames = (usedNames != null ? usedNames : new LinkedHashSet<String>()); } public BeanDefinitionHolder parseAsHolder(Element componentElement, ParserContext parserContext) { // save parser context this.parserContext = parserContext; this.defaults = new BlueprintDefaultsDefinition(componentElement.getOwnerDocument(), parserContext); // let Spring do its standard parsing BeanDefinitionHolder bdHolder = parseComponentDefinitionElement(componentElement, null); BeanDefinition bd = bdHolder.getBeanDefinition(); if (bd != null) { bd.setAttribute(ParsingUtils.BLUEPRINT_MARKER_NAME, Boolean.TRUE); } return bdHolder; } public BeanDefinition parse(Element componentElement, ParserContext parserContext) { return parseAsHolder(componentElement, parserContext).getBeanDefinition(); } /** * Parses the supplied <code><bean></code> element. May return <code>null</code> if there were errors during * parse. Errors are reported to the {@link org.springframework.beans.factory.parsing.ProblemReporter}. */ private BeanDefinitionHolder parseComponentDefinitionElement(Element ele, BeanDefinition containingBean) { // extract bean name String id = ele.getAttribute(BeanDefinitionParserDelegate.ID_ATTRIBUTE); String nameAttr = ele.getAttribute(BeanDefinitionParserDelegate.NAME_ATTRIBUTE); List<String> aliases = new ArrayList<String>(4); if (StringUtils.hasLength(nameAttr)) { String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, BeanDefinitionParserDelegate.BEAN_NAME_DELIMITERS); aliases.addAll(Arrays.asList(nameArr)); } String beanName = id; if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) { beanName = (String) aliases.remove(0); if (log.isDebugEnabled()) { log.debug("No XML 'id' specified - using '" + beanName + "' as bean name and " + aliases + " as aliases"); } } if (containingBean == null) { if (checkNameUniqueness(beanName, aliases, usedNames)) { error("Bean name '" + beanName + "' is already used in this file", ele); } if (ParsingUtils.isReservedName(beanName, ele, parserContext)) { error("Blueprint reserved name '" + beanName + "' cannot be used", ele); } } AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean); if (beanDefinition != null) { if (!StringUtils.hasText(beanName)) { try { if (containingBean != null) { beanName = ParsingUtils.generateBlueprintBeanName(beanDefinition, parserContext.getRegistry(), true); } else { beanName = ParsingUtils.generateBlueprintBeanName(beanDefinition, parserContext.getRegistry(), false); // TODO: should we support 2.0 behaviour (see below): // // Register an alias for the plain bean class name, if still possible, // if the generator returned the class name plus a suffix. // This is expected for Spring 1.2/2.0 backwards compatibility. } if (log.isDebugEnabled()) { log.debug("Neither XML 'id' nor 'name' specified - " + "using generated bean name [" + beanName + "]"); } } catch (Exception ex) { error(ex.getMessage(), ele, ex); return null; } } return new BeanDefinitionHolder(beanDefinition, beanName); } return null; } /** * Parse the bean definition itself, without regard to name or aliases. May return <code>null</code> if problems * occurred during the parse of the bean definition. */ private AbstractBeanDefinition parseBeanDefinitionElement(Element ele, String beanName, BeanDefinition containingBean) { this.parseState.push(new BeanEntry(beanName)); String className = null; if (ele.hasAttribute(BeanDefinitionParserDelegate.CLASS_ATTRIBUTE)) { className = ele.getAttribute(BeanDefinitionParserDelegate.CLASS_ATTRIBUTE).trim(); } try { AbstractBeanDefinition beanDefinition = BeanDefinitionReaderUtils.createBeanDefinition(null, className, parserContext.getReaderContext() .getBeanClassLoader()); // some early validation String activation = ele.getAttribute(LAZY_INIT_ATTR); String scope = ele.getAttribute(BeanDefinitionParserDelegate.SCOPE_ATTRIBUTE); if (EAGER_INIT_VALUE.equals(activation) && BeanDefinition.SCOPE_PROTOTYPE.equals(scope)) { error("Prototype beans cannot be eagerly activated", ele); } // add marker to indicate that the scope was present if (StringUtils.hasText(scope)) { beanDefinition.setAttribute(DECLARED_SCOPE, Boolean.TRUE); } // parse attributes parseAttributes(ele, beanName, beanDefinition); // inner beans get a predefined scope in RFC 124 if (containingBean != null) { beanDefinition.setLazyInit(true); beanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE); } // parse description beanDefinition.setDescription(DomUtils.getChildElementValueByTagName(ele, BeanDefinitionParserDelegate.DESCRIPTION_ELEMENT)); parseConstructorArgElements(ele, beanDefinition); parsePropertyElements(ele, beanDefinition); beanDefinition.setResource(parserContext.getReaderContext().getResource()); beanDefinition.setSource(extractSource(ele)); return beanDefinition; } catch (ClassNotFoundException ex) { error("Bean class [" + className + "] not found", ele, ex); } catch (NoClassDefFoundError err) { error("Class that bean class [" + className + "] depends on not found", ele, err); } catch (Throwable ex) { error("Unexpected failure during bean definition parsing", ele, ex); } finally { this.parseState.pop(); } return null; } private AbstractBeanDefinition parseAttributes(Element ele, String beanName, AbstractBeanDefinition beanDefinition) { AbstractBeanDefinition bd = parserContext.getDelegate().parseBeanDefinitionAttributes(ele, beanName, null, beanDefinition); // handle lazy flag (initialize) String lazyInit = ele.getAttribute(LAZY_INIT_ATTR); // check whether the value is "lazy" if (StringUtils.hasText(lazyInit)) { if (lazyInit.equalsIgnoreCase(LAZY_INIT_VALUE)) { bd.setLazyInit(true); } else { bd.setLazyInit(false); } } else { bd.setLazyInit(getDefaults(ele).getDefaultInitialization()); } // handle factory component String componentFactory = ele.getAttribute(FACTORY_REF_ATTR); if (StringUtils.hasText(componentFactory)) { bd.setFactoryBeanName(componentFactory); } // check whether the bean is a prototype with destroy method if (StringUtils.hasText(bd.getDestroyMethodName()) && BeanDefinition.SCOPE_PROTOTYPE.equalsIgnoreCase(bd.getScope())) { error("Blueprint prototype beans cannot define destroy methods", ele); } return bd; } /** * Validate that the specified bean name and aliases have not been used already. */ private boolean checkNameUniqueness(String beanName, Collection<String> aliases, Collection<String> usedNames) { String foundName = null; if (StringUtils.hasText(beanName) && usedNames.contains(beanName)) { foundName = beanName; } if (foundName == null) { foundName = (String) CollectionUtils.findFirstMatch(usedNames, aliases); } usedNames.add(beanName); usedNames.addAll(aliases); return (foundName != null); } /** * Parsers contructor arguments. * * @param ele * @param beanDefinition * @param parserContext */ private void parseConstructorArgElements(Element ele, AbstractBeanDefinition beanDefinition) { NodeList nl = ele.getChildNodes(); for (int i = 0; i < nl.getLength(); i++) { Node node = nl.item(i); if (node instanceof Element && DomUtils.nodeNameEquals(node, CONSTRUCTOR_ARG)) { parseConstructorArgElement((Element) node, beanDefinition); } } } private void parseConstructorArgElement(Element ele, AbstractBeanDefinition beanDefinition) { String indexAttr = ele.getAttribute(BeanDefinitionParserDelegate.INDEX_ATTRIBUTE); String typeAttr = ele.getAttribute(BeanDefinitionParserDelegate.TYPE_ATTRIBUTE); boolean hasIndex = false; int index = -1; if (StringUtils.hasLength(indexAttr)) { hasIndex = true; try { index = Integer.parseInt(indexAttr); } catch (NumberFormatException ex) { error("Attribute 'index' of tag 'constructor-arg' must be an integer", ele); } if (index < 0) { error("'index' cannot be lower than 0", ele); } } try { this.parseState.push(hasIndex ? new ConstructorArgumentEntry(index) : new ConstructorArgumentEntry()); ConstructorArgumentValues values = beanDefinition.getConstructorArgumentValues(); // Blueprint failure (index duplication) Integer indexInt = Integer.valueOf(index); if (values.getIndexedArgumentValues().containsKey(indexInt)) { error("duplicate 'index' with value=[" + index + "] specified", ele); } Object value = parsePropertyValue(ele, beanDefinition, null); ConstructorArgumentValues.ValueHolder valueHolder = new ConstructorArgumentValues.ValueHolder(value); if (StringUtils.hasLength(typeAttr)) { valueHolder.setType(typeAttr); } valueHolder.setSource(extractSource(ele)); if (hasIndex) { values.addIndexedArgumentValue(index, valueHolder); } else { values.addGenericArgumentValue(valueHolder); } // Blueprint failure (mixed index/non-indexed arguments) if (!values.getGenericArgumentValues().isEmpty() && !values.getIndexedArgumentValues().isEmpty()) { error("indexed and non-indexed constructor arguments are not supported by Blueprint; " + "consider using the Spring namespace instead", ele); } } finally { this.parseState.pop(); } } /** * Parses property elements. * * @param ele * @param beanDefinition * @param parserContext */ private void parsePropertyElements(Element ele, AbstractBeanDefinition beanDefinition) { NodeList nl = ele.getChildNodes(); for (int i = 0; i < nl.getLength(); i++) { Node node = nl.item(i); if (node instanceof Element && DomUtils.nodeNameEquals(node, BeanDefinitionParserDelegate.PROPERTY_ELEMENT)) { parsePropertyElement((Element) node, beanDefinition); } } } private void parsePropertyElement(Element ele, BeanDefinition bd) { String propertyName = ele.getAttribute(BeanDefinitionParserDelegate.NAME_ATTRIBUTE); if (!StringUtils.hasLength(propertyName)) { error("Tag 'property' must have a 'name' attribute", ele); return; } this.parseState.push(new PropertyEntry(propertyName)); try { if (bd.getPropertyValues().contains(propertyName)) { error("Multiple 'property' definitions for property '" + propertyName + "'", ele); return; } Object val = parsePropertyValue(ele, bd, propertyName); PropertyValue pv = new PropertyValue(propertyName, val); pv.setSource(parserContext.extractSource(ele)); bd.getPropertyValues().addPropertyValue(pv); } finally { this.parseState.pop(); } } private Object parsePropertyValue(Element ele, BeanDefinition bd, String propertyName) { String elementName = (propertyName != null) ? "<property> element for property '" + propertyName + "'" : "<constructor-arg> element"; // Should only have one child element: ref, value, list, etc. NodeList nl = ele.getChildNodes(); Element subElement = null; for (int i = 0; i < nl.getLength(); i++) { Node node = nl.item(i); if (node instanceof Element && !DomUtils.nodeNameEquals(node, BeanDefinitionParserDelegate.DESCRIPTION_ELEMENT)) { // Child element is what we're looking for. if (subElement != null) { error(elementName + " must not contain more than one sub-element", ele); } else { subElement = (Element) node; } } } boolean hasRefAttribute = ele.hasAttribute(BeanDefinitionParserDelegate.REF_ATTRIBUTE); boolean hasValueAttribute = ele.hasAttribute(BeanDefinitionParserDelegate.VALUE_ATTRIBUTE); if ((hasRefAttribute && hasValueAttribute) || ((hasRefAttribute || hasValueAttribute) && subElement != null)) { error(elementName + " is only allowed to contain either 'ref' attribute OR 'value' attribute OR sub-element", ele); } if (hasRefAttribute) { String refName = ele.getAttribute(BeanDefinitionParserDelegate.REF_ATTRIBUTE); if (!StringUtils.hasText(refName)) { error(elementName + " contains empty 'ref' attribute", ele); } RuntimeBeanReference ref = new RuntimeBeanReference(refName); ref.setSource(parserContext.extractSource(ele)); return ref; } else if (hasValueAttribute) { TypedStringValue valueHolder = new TypedStringValue(ele.getAttribute(BeanDefinitionParserDelegate.VALUE_ATTRIBUTE)); valueHolder.setSource(parserContext.extractSource(ele)); return valueHolder; } else if (subElement != null) { return parsePropertySubElement(subElement, bd, null); } else { // Neither child element nor "ref" or "value" attribute found. error(elementName + " must specify a ref or value", ele); return null; } } public static Object parsePropertySubElement(ParserContext parserContext, Element ele, BeanDefinition bd) { return new BlueprintParser(parserContext).parsePropertySubElement(ele, bd, null); } public static Map<?, ?> parsePropertyMapElement(ParserContext parserContext, Element ele, BeanDefinition bd) { return new BlueprintParser(parserContext).parseMapElement(ele, bd); } public static Set<?> parsePropertySetElement(ParserContext parserContext, Element ele, BeanDefinition bd) { return new BlueprintParser(parserContext).parseSetElement(ele, bd); } /** * Parse a value, ref or collection sub-element of a property or constructor-arg element. This method is called from * several places to handle reusable elements such as idref, ref, null, value and so on. * * In fact, this method is the main reason why the BeanDefinitionParserDelegate is not used in full since the * element namespace becomes important as mixed rfc124/bean content can coexist. * * @param ele subelement of property element; we don't know which yet * @param defaultValueType the default type (class name) for any <code><value></code> tag that might be * created */ private Object parsePropertySubElement(Element ele, BeanDefinition bd, String defaultValueType) { // skip other namespace String namespaceUri = ele.getNamespaceURI(); // check Spring own namespace if (parserContext.getDelegate().isDefaultNamespace(namespaceUri)) { return parserContext.getDelegate().parsePropertySubElement(ele, bd); } // let the delegate handle other ns else if (!NAMESPACE_URI.equals(namespaceUri)) { return parserContext.getDelegate().parseCustomElement(ele); } // else { if (DomUtils.nodeNameEquals(ele, BEAN)) { BeanDefinitionHolder bdHolder = parseComponentDefinitionElement(ele, bd); if (bdHolder != null) { bdHolder = ParsingUtils.decorateBeanDefinitionIfRequired(ele, bdHolder, parserContext); } return bdHolder; } if (DomUtils.nodeNameEquals(ele, BeanDefinitionParserDelegate.REF_ELEMENT)) { return parseRefElement(ele); } else if (DomUtils.nodeNameEquals(ele, BeanDefinitionParserDelegate.IDREF_ELEMENT)) { return parseIdRefElement(ele); } else if (DomUtils.nodeNameEquals(ele, BeanDefinitionParserDelegate.VALUE_ELEMENT)) { return parseValueElement(ele, defaultValueType); } else if (DomUtils.nodeNameEquals(ele, BeanDefinitionParserDelegate.NULL_ELEMENT)) { // It's a distinguished null value. Let's wrap it in a TypedStringValue // object in order to preserve the source location. TypedStringValue nullHolder = new TypedStringValue(null); nullHolder.setSource(parserContext.extractSource(ele)); return nullHolder; } else if (DomUtils.nodeNameEquals(ele, BeanDefinitionParserDelegate.ARRAY_ELEMENT)) { return parseArrayElement(ele, bd); } else if (DomUtils.nodeNameEquals(ele, BeanDefinitionParserDelegate.LIST_ELEMENT)) { return parseListElement(ele, bd); } else if (DomUtils.nodeNameEquals(ele, BeanDefinitionParserDelegate.SET_ELEMENT)) { return parseSetElement(ele, bd); } else if (DomUtils.nodeNameEquals(ele, BeanDefinitionParserDelegate.MAP_ELEMENT)) { return parseMapElement(ele, bd); } else if (DomUtils.nodeNameEquals(ele, BeanDefinitionParserDelegate.PROPS_ELEMENT)) { return parsePropsElement(ele); } // maybe it's a nested service/reference/ref-list/ref-set return parserContext.getDelegate().parseCustomElement(ele, bd); } } private Object parseRefElement(Element ele) { // A generic reference to any name of any component. String refName = ele.getAttribute(COMPONENT_ID_ATTR); if (!StringUtils.hasLength(refName)) { error("'" + COMPONENT_ID_ATTR + "' is required for <ref> element", ele); return null; } if (!StringUtils.hasText(refName)) { error("<ref> element contains empty target attribute", ele); return null; } RuntimeBeanReference ref = new InstanceEqualityRuntimeBeanReference(refName); ref.setSource(parserContext.extractSource(ele)); return ref; } private Object parseIdRefElement(Element ele) { // A generic reference to any name of any bean/component. String refName = ele.getAttribute(COMPONENT_ID_ATTR); if (!StringUtils.hasLength(refName)) { error("'" + COMPONENT_ID_ATTR + "' is required for <idref> element", ele); return null; } if (!StringUtils.hasText(refName)) { error("<idref> element contains empty target attribute", ele); return null; } RuntimeBeanNameReference ref = new RuntimeBeanNameReference(refName); ref.setSource(parserContext.extractSource(ele)); return ref; } /** * Return a typed String value Object for the given value element. * * @param ele element * @param defaultTypeName type class name * @return typed String value Object */ private Object parseValueElement(Element ele, String defaultTypeName) { // It's a literal value. String value = DomUtils.getTextValue(ele); String specifiedTypeName = ele.getAttribute(BeanDefinitionParserDelegate.TYPE_ATTRIBUTE); String typeName = specifiedTypeName; if (!StringUtils.hasText(typeName)) { typeName = defaultTypeName; } try { TypedStringValue typedValue = buildTypedStringValue(value, typeName); typedValue.setSource(extractSource(ele)); typedValue.setSpecifiedTypeName(specifiedTypeName); return typedValue; } catch (ClassNotFoundException ex) { error("Type class [" + typeName + "] not found for <value> element", ele, ex); return value; } } /** * Build a typed String value Object for the given raw value. * * @see org.springframework.beans.factory.config.TypedStringValue */ private TypedStringValue buildTypedStringValue(String value, String targetTypeName) throws ClassNotFoundException { ClassLoader classLoader = parserContext.getReaderContext().getBeanClassLoader(); TypedStringValue typedValue; if (!StringUtils.hasText(targetTypeName)) { typedValue = new TypedStringValue(value); } else if (classLoader != null) { Class<?> targetType = ClassUtils.forName(targetTypeName, classLoader); typedValue = new TypedStringValue(value, targetType); } else { typedValue = new TypedStringValue(value, targetTypeName); } return typedValue; } /** * Parse an array element. */ public Object parseArrayElement(Element arrayEle, BeanDefinition bd) { String elementType = arrayEle.getAttribute(BeanDefinitionParserDelegate.VALUE_TYPE_ATTRIBUTE); NodeList nl = arrayEle.getChildNodes(); ManagedArray target = new ManagedArray(elementType, nl.getLength()); target.setSource(extractSource(arrayEle)); target.setElementTypeName(elementType); target.setMergeEnabled(parseMergeAttribute(arrayEle)); parseCollectionElements(nl, target, bd, elementType); return target; } /** * Parse a list element. */ public List<?> parseListElement(Element collectionEle, BeanDefinition bd) { String defaultElementType = collectionEle.getAttribute(BeanDefinitionParserDelegate.VALUE_TYPE_ATTRIBUTE); NodeList nl = collectionEle.getChildNodes(); ManagedList<Object> target = new ManagedList<Object>(nl.getLength()); target.setSource(extractSource(collectionEle)); target.setElementTypeName(defaultElementType); target.setMergeEnabled(parseMergeAttribute(collectionEle)); parseCollectionElements(nl, target, bd, defaultElementType); return target; } /** * Parse a set element. */ public Set<?> parseSetElement(Element collectionEle, BeanDefinition bd) { String defaultElementType = collectionEle.getAttribute(BeanDefinitionParserDelegate.VALUE_TYPE_ATTRIBUTE); NodeList nl = collectionEle.getChildNodes(); ManagedSet<Object> target = new ManagedSet<Object>(nl.getLength()); target.setSource(extractSource(collectionEle)); target.setElementTypeName(defaultElementType); target.setMergeEnabled(parseMergeAttribute(collectionEle)); parseCollectionElements(nl, target, bd, defaultElementType); return target; } protected void parseCollectionElements(NodeList elementNodes, Collection<Object> target, BeanDefinition bd, String defaultElementType) { for (int i = 0; i < elementNodes.getLength(); i++) { Node node = elementNodes.item(i); if (node instanceof Element && !DomUtils.nodeNameEquals(node, BeanDefinitionParserDelegate.DESCRIPTION_ELEMENT)) { target.add(parsePropertySubElement((Element) node, bd, defaultElementType)); } } } /** * Parse a map element. */ public Map<?, ?> parseMapElement(Element mapEle, BeanDefinition bd) { String defaultKeyType = mapEle.getAttribute(BeanDefinitionParserDelegate.KEY_TYPE_ATTRIBUTE); String defaultValueType = mapEle.getAttribute(BeanDefinitionParserDelegate.VALUE_TYPE_ATTRIBUTE); List<Element> entryEles = DomUtils.getChildElementsByTagName(mapEle, BeanDefinitionParserDelegate.ENTRY_ELEMENT); ManagedMap<Object, Object> map = new ManagedMap<Object, Object>(entryEles.size()); map.setSource(extractSource(mapEle)); map.setKeyTypeName(defaultKeyType); map.setValueTypeName(defaultValueType); map.setMergeEnabled(parseMergeAttribute(mapEle)); for (Element entryEle : entryEles) { // Should only have one value child element: ref, value, list, etc. // Optionally, there might be a key child element. NodeList entrySubNodes = entryEle.getChildNodes(); Element keyEle = null; Element valueEle = null; for (int j = 0; j < entrySubNodes.getLength(); j++) { Node node = entrySubNodes.item(j); if (node instanceof Element) { Element candidateEle = (Element) node; if (DomUtils.nodeNameEquals(candidateEle, BeanDefinitionParserDelegate.KEY_ELEMENT)) { if (keyEle != null) { error("<entry> element is only allowed to contain one <key> sub-element", entryEle); } else { keyEle = candidateEle; } } else { // Child element is what we're looking for. if (valueEle != null) { error("<entry> element must not contain more than one value sub-element", entryEle); } else { valueEle = candidateEle; } } } } // Extract key from attribute or sub-element. Object key = null; boolean hasKeyAttribute = entryEle.hasAttribute(BeanDefinitionParserDelegate.KEY_ATTRIBUTE); boolean hasKeyRefAttribute = entryEle.hasAttribute(BeanDefinitionParserDelegate.KEY_REF_ATTRIBUTE); if ((hasKeyAttribute && hasKeyRefAttribute) || ((hasKeyAttribute || hasKeyRefAttribute)) && keyEle != null) { error("<entry> element is only allowed to contain either " + "a 'key' attribute OR a 'key-ref' attribute OR a <key> sub-element", entryEle); } if (hasKeyAttribute) { key = buildTypedStringValueForMap(entryEle.getAttribute(BeanDefinitionParserDelegate.KEY_ATTRIBUTE), defaultKeyType, entryEle); } else if (hasKeyRefAttribute) { String refName = entryEle.getAttribute(BeanDefinitionParserDelegate.KEY_REF_ATTRIBUTE); if (!StringUtils.hasText(refName)) { error("<entry> element contains empty 'key-ref' attribute", entryEle); } RuntimeBeanReference ref = new RuntimeBeanReference(refName); ref.setSource(extractSource(entryEle)); key = ref; } else if (keyEle != null) { key = parseKeyElement(keyEle, bd, defaultKeyType); } else { error("<entry> element must specify a key", entryEle); } // Extract value from attribute or sub-element. Object value = null; boolean hasValueAttribute = entryEle.hasAttribute(BeanDefinitionParserDelegate.VALUE_ATTRIBUTE); boolean hasValueRefAttribute = entryEle.hasAttribute(BeanDefinitionParserDelegate.VALUE_REF_ATTRIBUTE); if ((hasValueAttribute && hasValueRefAttribute) || ((hasValueAttribute || hasValueRefAttribute)) && valueEle != null) { error("<entry> element is only allowed to contain either " + "'value' attribute OR 'value-ref' attribute OR <value> sub-element", entryEle); } if (hasValueAttribute) { value = buildTypedStringValueForMap( entryEle.getAttribute(BeanDefinitionParserDelegate.VALUE_ATTRIBUTE), defaultValueType, entryEle); } else if (hasValueRefAttribute) { String refName = entryEle.getAttribute(BeanDefinitionParserDelegate.VALUE_REF_ATTRIBUTE); if (!StringUtils.hasText(refName)) { error("<entry> element contains empty 'value-ref' attribute", entryEle); } RuntimeBeanReference ref = new RuntimeBeanReference(refName); ref.setSource(extractSource(entryEle)); value = ref; } else if (valueEle != null) { value = parsePropertySubElement(valueEle, bd, defaultValueType); } else { error("<entry> element must specify a value", entryEle); } // Add final key and value to the Map. map.put(key, value); } return map; } /** * Parse a props element. */ public Properties parsePropsElement(Element propsEle) { ManagedProperties props = new OrderedManagedProperties(); props.setSource(extractSource(propsEle)); props.setMergeEnabled(parseMergeAttribute(propsEle)); List propEles = DomUtils.getChildElementsByTagName(propsEle, BeanDefinitionParserDelegate.PROP_ELEMENT); for (Iterator it = propEles.iterator(); it.hasNext();) { Element propEle = (Element) it.next(); String key = propEle.getAttribute(BeanDefinitionParserDelegate.KEY_ATTRIBUTE); // Trim the text value to avoid unwanted whitespace // caused by typical XML formatting. String value = DomUtils.getTextValue(propEle).trim(); TypedStringValue keyHolder = new TypedStringValue(key); keyHolder.setSource(extractSource(propEle)); TypedStringValue valueHolder = new TypedStringValue(value); valueHolder.setSource(extractSource(propEle)); props.put(keyHolder, valueHolder); } return props; } private boolean parseMergeAttribute(Element element) { return parserContext.getDelegate().parseMergeAttribute(element); } /** * Build a typed String value Object for the given raw value. * * @see org.springframework.beans.factory.config.TypedStringValue */ private Object buildTypedStringValueForMap(String value, String defaultTypeName, Element entryEle) { try { TypedStringValue typedValue = buildTypedStringValue(value, defaultTypeName); typedValue.setSource(extractSource(entryEle)); return typedValue; } catch (ClassNotFoundException ex) { error("Type class [" + defaultTypeName + "] not found for Map key/value type", entryEle, ex); return value; } } /** * Parse a key sub-element of a map element. */ private Object parseKeyElement(Element keyEle, BeanDefinition bd, String defaultKeyTypeName) { NodeList nl = keyEle.getChildNodes(); Element subElement = null; for (int i = 0; i < nl.getLength(); i++) { Node node = nl.item(i); if (node instanceof Element) { // Child element is what we're looking for. if (subElement != null) { error("<key> element must not contain more than one value sub-element", keyEle); } else { subElement = (Element) node; } } } return parsePropertySubElement(subElement, bd, defaultKeyTypeName); } // util methods (used as shortcuts) private Object extractSource(Element ele) { return parserContext.extractSource(ele); } /** * Reports an error with the given message for the given source element. */ private void error(String message, Node source) { parserContext.getReaderContext().error(message, source, parseState.snapshot()); } /** * Reports an error with the given message for the given source element. */ private void error(String message, Node source, Throwable cause) { parserContext.getReaderContext().error(message, source, parseState.snapshot(), cause); } private BlueprintDefaultsDefinition getDefaults(Element ele) { if (defaults == null) { defaults = new BlueprintDefaultsDefinition(ele.getOwnerDocument(), parserContext); } return defaults; } }